C++复习 引用

左值和右值

  • 左值和右值
    • 辣鸡的C++ PRIMER解释
      • 左值(lvalue) : 是那些求值结果为对象或者函数的表达式,一个表示对象的非常量左值可以作为赋值运算符的左侧运算对象。
      • 右值(rvalue) : 是指一种表达式,其结果是值而非值所在的位置。
    • 参考博客:https://blog.csdn.net/xuwqiang1994/article/details/79924310
      • 左值(lvalue) : lvalue(locator value)代表一个在内存中占有确定位置的对象(换句话说就是有一个地址)。
      • 右值(rvalue) : rvalue通过排他性来定义,每个表达式不是lvalue就是rvalue。因此从上面的lvalue的定义,rvalue是不在内存中占有确定位置的表达式。
    • 参考博客:https://blog.csdn.net/qq_24964575/article/details/52202989
      • 左值可以当作右值使用,右值不能当左值用.
      • 左值可以出现在=的任何一方,
      • 出现在=右方的左值,是把左值当作右值使用,这是非常正常的.
      • 右值,不能当作左值使用.
      • 能够(单独)出现在=左方的是左值(实际是左右值,左值总是可以当作右值使用)
      • 只能够出现在=右方的,是右值(纯粹的右值),不能当作左值使用.
      • 左值代表地址单元,右值代表数据本身.
      • 地址单元里的数据,是左值变量的右值的含义.
      • 右值包括,纯数据,比如常数100,字符串常量”12345”等
      • 以及地址单元中存储的数据.比如x ,这是左值当作右值使用代表的意义.
      • 以及一些常变量(有确切地址的常量,可以象变量一样有个名字,其实就是变量,只是右值化了,不能当左值使用了)的含义.
      • 左值代表存储器的一个单元.
      • 这是一个物理概念,不仅仅是个地址(地址不过是个编号,是个数值而已),是个实实在在的东西.
      • 右值代表数据,一个数值而已.
    • 参考博客:https://www.zhihu.com/question/382300648
      • 取地址符取到的是一个地址,没有实际存储在哪,所以是右值
      • 如果是寄存器变量也没有地址,所以是右值

使用decltype时,左值返回引用类型,右值不返回引用类型。

  • 对于自增自减(++i,i++)
    • ++i将对象本身作为左值返回
    • i++将对象原始值作为右值返回

右值引用(c++)

int &&rr1 = 42;//true
int &&rr2 = rr1;//error
  • move函数:获得绑定在左值上的右值引用
    • 调用move后,原变量除赋值和销毁外,不能再使用他

右值引用作用

  • 右值引用可实现转移语义(Move Sementics)和精确传递(Perfect Forwarding),它的主要目的有两个方面:
    • 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
    • 能够更简洁明确地定义泛型函数。

引用折叠

C++引用折叠

  • X& &、X& &&、X&& & 可折叠成 X&
  • X&& && 可折叠成 X&&

右值引用详解

  • 参考:[c++11]我理解的右值引用、移动语义和完美转发

    移动语义(Move Sementics)— 移动构造和移动赋值

  • 对于如下场景
    • 使用拷贝构造函数对string(“hello”)进行构造,会造成没有意义的资源申请和释放操作
    • 而移动语义可以直接使用临时对象已经申请的资源,既能节省资源,又能节省资源申请和释放的时间
  // 拷贝构造函数
   MyString(const MyString& str) {
       CCtor ++;
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
   }
   // 移动构造函数
   MyString(MyString&& str) noexcept
       :m_data(str.m_data) {
       MCtor ++;
       str.m_data = nullptr; //不再指向之前的资源了
   }

   // 拷贝赋值函数 =号重载
   MyString& operator=(const MyString& str){
       CAsgn ++;
       if (this == &str) // 避免自我赋值!!
          return *this;

       delete[] m_data;
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
       return *this;
   }

   // 移动赋值函数 =号重载
   MyString& operator=(MyString&& str) noexcept{
       MAsgn ++;
       if (this == &str) // 避免自我赋值!!
          return *this;

       delete[] m_data;
       m_data = str.m_data;
       str.m_data = nullptr; //不再指向之前的资源了
       return *this;
   }
  • 对于String("hello")它传入的参数是个临时对象,是右值,优先进入移动构造函数而不是拷贝构造函数
    • 拷贝构造函数是重新分配一块内存空间,将要拷贝的对象复制过来
    • 因为对于构造的右值马上就会销毁,所以移动构造函数会将自己的指针指向内存中已有的资源.并将本来指向这个资源的指针置为nullptr

std::move()

  • 对于一些生命周期短的局部左值,如果需要更充分的利用起来,可以使用std::move()将左值转换为右值,从而方便应用移动语义
  • std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义
  • 转换所有权,注意是转换,不发生深拷贝!

    std::move()底层实现

    template <typename T>
    typename remove_reference<T>::type&& move(T&& t)
    {
      return static_cast<typename remove_reference<T>::type &&>(t);
    }
    
  • 1.对于C++的模板存在通用引用的特性,然后根据引用折叠,将右值经过 T&& 传递类型保持不变还是右值,而左值经过 T&& 变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变。
  • 2.然后根据模板偏特化,获取左值引用和右值引用的具体类型.
  • 3.利用static_cast<>进行强制类型转换,返回右值引用

universal references(通用引用) — 引用折叠

  • 当右值引用和模板结合的时候.T&&并不一定表示右值引用,它可能是个左值引用又可能是个右值引用.

    • 这时&&是一个未定义的引用类型,称为universal references
  • 只有当发生自动类型推断时(如函数模板的类型自动推导,或auto关键字),&&才是一个universal references。

template<typename T>
void f( T&& param); //这里T的类型需要推导,所以&&是一个 universal references

template<typename T>
class Test {
  Test(Test&& rhs); //Test是一个特定的类型,不需要类型推导,所以&&表示右值引用  
};

void f(Test&& param); //右值引用

//复杂一点
template<typename T>
void f(std::vector<T>&& param); //在调用这个函数之前,这个vector<T>中的推断类型
//已经确定了,所以调用f函数的时候没有类型推断了,所以是 右值引用

template<typename T>
void f(const T&& param); //右值引用
// universal references仅仅发生在 T&& 下面,任何一点附加条件都会使之失效

引用折叠

  • T被推导成了string, T&&就是string&&
  • T被推导成了string&, T&&就是string& &&,这将会发生引用折叠

  • 规则如下:

    • 所有的右值引用叠加到右值引用上仍然使一个右值引用。
    • 有的其他引用类型之间的叠加都将变成左值引用。

完美转发

  • 转发:就是通过一个函数将参数继续转交给另一个函数进行处理
  • 完美:原参数可能是右值,可能是左值,转发后保持参数的原有特征
void process(int& i){
    cout << "process(int&):" << i << endl;
}
void process(int&& i){
    cout << "process(int&&):" << i << endl;
}

void myforward(int&& i){
    cout << "myforward(int&&):" << i << endl;
    process(i);
}

int main()
{
    int a = 0;
    process(a); //a被视为左值 process(int&):0
    process(1); //1被视为右值 process(int&&):1
    process(move(a)); //强制将a由左值改为右值 process(int&&):0
    myforward(2);  //右值经过forward函数转交给process函数,却称为了一个左值,
    //原因是该右值有了名字  所以是 process(int&):2
    myforward(move(a));  // 同上,在转发的时候右值变成了左值  process(int&):0
    // myforward(a) // 错误用法,右值引用不接受左值
}

std::forward()

  • 通过std::forward()可以完全转发右值
    ```cpp
    void myforward(int&& i){
    cout << “myforward(int&&):” << i << endl;
    process(std::forward(i));
    }

myforward(2); // process(int&&):2


* 对于同时解决左右值的完美转发,需要借助universal references通用引用类型和std::forward()模板函数共同实现完美转发.

```cpp
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;

void RunCode(int &&m) {
    cout << "rvalue ref" << endl;
}
void RunCode(int &m) {
    cout << "lvalue ref" << endl;
}
void RunCode(const int &&m) {
    cout << "const rvalue ref" << endl;
}
void RunCode(const int &m) {
    cout << "const lvalue ref" << endl;
}

// 这里利用了universal references,如果写T&,就不支持传入右值,而写T&&,既能支持左值,又能支持右值
template<typename T>
void perfectForward(T && t) {
    RunCode(forward<T> (t));
}

template<typename T>
void notPerfectForward(T && t) {
    RunCode(t);
}